﻿using log4net;
using log4net.Appender;
using log4net.Config;
using log4net.Core;
using log4net.Layout;
using Microsoft.Win32;
using System;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.Security.Permissions;
using System.Text;

namespace DW.Library
{
    public enum LogLevel
    {
        OFF = 0,
        FATAL = 1,
        ERROR = 2,
        WARN = 3,
        INFO = 4,
        DEBUG = 5,
        NOUSE = 10,
    }

    public class Logger
    {
        private log4net.Repository.ILoggerRepository _repository = null;

        public static Logger Instance
        {
            get
            {
                if (_sInstance == null)
                {
                    lock (_slock)
                    {
                        if (_sInstance == null)
                        {
                            _sInstance = new Logger();
                        }
                    }
                }

                return _sInstance;
            }
        }

        /// <summary>
        /// Write a log at warning level
        /// </summary>
        /// <param name="logTag">log tag</param>
        /// <param name="message">Warning message</param>
        /// <param name="args">Warning message</param>
        public void Warn(string logTag, string message, params object[] args)
        {
            if (_isLoaded)
                Warn(logTag, string.Empty, message, args);
        }

        /// <summary>
        /// Write a log at warning level
        /// </summary>
        /// <param name="className">Name of the class where the warning happens</param>
        /// <param name="methodName">Name of the function where the warning happens</param>
        /// <param name="message">Warning message</param>
        /// <param name="args">Parameters of the warning message</param>
        public void Warn(string className, string methodName, string message, params object[] args)
        {
            if (_isLoaded && _log.IsWarnEnabled)
            {
                var formattedMessage = FormatMessage(message, args);
                _log.WarnFormat(messageFormat, className, methodName, formattedMessage);
            }
        }


        /// <summary>
        /// Write a log at fatal level
        /// </summary>
        /// <param name="logTag">log tag</param>
        /// <param name="message">Warning message</param>
        /// <param name="args">Warning message</param>
        public void Fatal(string logTag, string message, params object[] args)
        {
            if (_isLoaded)
                Fatal(logTag, string.Empty, message, args);
        }

        /// <summary>
        /// Write a log at fatal level
        /// </summary>
        /// <param name="className">Name of the class where the fatal event happens</param>
        /// <param name="methodName">Name of the function where the fatal event happens</param>
        /// <param name="message">Fatal message</param>
        /// <param name="args">Parameters of the fatal message</param>
        public void Fatal(string className, string methodName, string message, params object[] args)
        {
            try
            {
                if (_isLoaded && _log.IsFatalEnabled)
                {
                    var formattedMessage = FormatMessage(message, args);
                    _log.FatalFormat(messageFormat, className, methodName, formattedMessage);
                }
            }
            catch
            {

            }
        }

        /// <summary>
        /// Write a log at infomation level
        /// </summary>
        /// <param name="className">Name of the class where the event happens</param>
        /// <param name="methodName">Name of the function where the event happens</param>
        /// <param name="message">Information message</param>
        /// <param name="args">Parameters of the information message</param>
        public void Info(string className, string methodName, string message, params object[] args)
        {
            try
            {
                if (_isLoaded && _log.IsInfoEnabled)
                {
                    var formattedMessage = FormatMessage(message, args);
                    _log.InfoFormat(messageFormat, className, methodName, formattedMessage);
                }
            }
            catch
            {

            }
        }

        /// <summary>
        /// Write a log at infomation level
        /// </summary>
        /// <param name="logTag">log tag</param>
        /// <param name="message">Information message</param>
        /// <param name="args">Parameters of the information message</param>
        public void Info(string logTag, string message, params object[] args)
        {
            if (_isLoaded)
                Info(logTag, string.Empty, message, args);
        }

        /// <summary>
        /// Write a log at error level
        /// </summary>
        /// <param name="className">Name of the class where the error happens</param>
        /// <param name="methodName">Name of the function where the error happens</param>
        /// <param name="message">Error message</param>
        /// <param name="args">Parameters of the error message</param>
        public void Error(string className, string methodName, string message, params object[] args)
        {
            try
            {
                if (_isLoaded && _log.IsErrorEnabled)
                {
                    var formattedMessage = FormatMessage(message, args);
                    _log.ErrorFormat(messageFormat, className, methodName, formattedMessage);
                }
            }
            catch
            {

            }
        }

        /// <summary>
        /// Write a log at error level
        /// </summary>
        /// <param name="logTag">log tag</param>
        /// <param name="message">Error message</param>
        /// <param name="args">Parameters of the error message</param>
        public void Error(string logTag, string message, params object[] args)
        {
            if (_isLoaded)
                Error(logTag, string.Empty, message, args);
        }

        /// <summary>
        /// Write a log at debug level
        /// </summary>
        /// <param name="className">Name of the class where the event happens</param>
        /// <param name="methodName">Name of the function where the event happens</param>
        /// <param name="message">Debug message</param>
        /// <param name="args">Parameters of the debug message</param>
        public void Debug(string className, string methodName, string message, params object[] args)
        {
            try
            {
                if (_isLoaded && _log.IsDebugEnabled)
                {
                    var formattedMessage = FormatMessage(message, args);
                    _log.DebugFormat(messageFormat, className, methodName, formattedMessage);
                }
            }
            catch
            {

            }
        }

        /// <summary>
        /// Write a log at debug level
        /// </summary>
        /// <param name="logTag">log tag</param>
        /// <param name="message">Debug message</param>
        /// <param name="args">Parameters of the debug message</param>
        public void Debug(string logTag, string message, params object[] args)
        {
            if (_isLoaded)
                Debug(logTag, string.Empty, message, args);
        }

        /// <summary>
        /// Dump object to log
        /// </summary>
        /// <param name="className"></param>
        /// <param name="methodName"></param>
        /// <param name="target"></param>
        public void DumpObject(string className, string methodName, object target)
        {
            if (_isLoaded && _log.IsDebugEnabled && target != null)
            {
                StringBuilder builder = new StringBuilder();
                builder.AppendFormat("{0}:{1}", "DumpObject", target);
                try
                {
                    Type type = target.GetType();
                    var properties = type.GetProperties(BindingFlags.Public | BindingFlags.Instance);
                    object[] firstValue = new object[0];

                    foreach (var property in properties)
                    {
                        if (property.CanRead)
                        {
                            try
                            {
                                builder.AppendFormat("\r\n\t{0}:{1}", property.Name,
                                    property.GetValue(target, firstValue));
                            }
                            catch
                            {
                                builder.AppendFormat("\r\n\t{0}:{1}", property.Name, "Faied to get property value");
                            }
                        }
                    }
                }
                catch
                {
                    builder.Append("\r\n\tFailed to dump object");
                }

                try
                {
                    _log.DebugFormat(messageFormat, className, methodName, builder);
                }
                catch
                {

                }
            }
        }

        /// <summary>
        /// Assert
        /// </summary>
        /// <param name="condition">assert condition</param>
        [Conditional("DEBUG")]
        public void Assert(bool condition)
        {
            if (_isLoaded && _log.IsDebugEnabled && !condition)
            {
                System.Diagnostics.Debug.Assert(condition);
            }
        }

        /// <summary>
        /// Assert
        /// </summary>
        /// <param name="condition">assert condition</param>
        /// <param name="message">message</param>
        [Conditional("DEBUG")]
        public void Assert(bool condition, string message)
        {
            if (_isLoaded && _log.IsDebugEnabled && !condition)
            {
                System.Diagnostics.Debug.Assert(condition, message);
            }
        }

        /// <summary>
        /// Assert
        /// </summary>
        /// <param name="condition">assert condition</param>
        /// <param name="message">message</param>
        /// <param name="detailMessage">detail message</param>
        [Conditional("DEBUG")]
        public void Assert(bool condition, string message, string detailMessage)
        {
            if (_isLoaded && _log.IsDebugEnabled && !condition)
            {
                System.Diagnostics.Debug.Assert(condition, message, detailMessage);
            }
        }

        private string FormatMessage(string message, params object[] args)
        {
            if (args == null || args.Length == 0)
            {
                return message;
            }
            else
            {
                try
                {
                    return string.Format(message, args);
                }
                catch
                {
                    return message;
                }
            }
        }

        /// <summary>
        /// Private constructor
        /// </summary>
        private Logger()
        {
        }

        public const string RollableSolutionKeyPath = @"SOFTWARE\Lenovo\RollableSolution";

        public string AppdataPath = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) + $"\\Lenovo\\RollableSolution";


        public void CreateLogger(string processName, LogLevel level = LogLevel.DEBUG)
        {
            try
            {
                var logLevel = level;
                var currentAppdataPath = Path.Combine(AppdataPath, processName);
                Logger.Instance.LoadConfig(currentAppdataPath + $"\\log\\", processName, logLevel);
            }
            catch
            { }
        }

        public bool CreateLogger(string processName)
        {
            Microsoft.Win32.RegistryKey key = null;
            try
            {
                string sLevel = String.Empty;
                key = Microsoft.Win32.Registry.CurrentUser.OpenSubKey(Path.Combine(RollableSolutionKeyPath, processName));
                if (key != null)
                {
                    sLevel = key.GetValue("logLevel") as string;
                }
                if (key != null && !string.IsNullOrEmpty(sLevel) || true)
                //if (sLevel != null)
                {
                    LogLevel logLevel = LogLevel.DEBUG;
                    switch (sLevel)
                    {
                        case "DEBUG":
                            logLevel = LogLevel.DEBUG;
                            break;
                        case "ERROR":
                            logLevel = LogLevel.ERROR;
                            break;
                        case "FATAL":
                            logLevel = LogLevel.FATAL;
                            break;
                        case "WARN":
                            logLevel = LogLevel.WARN;
                            break;
                        case "INFO":
                            logLevel = LogLevel.INFO;
                            break;
                        case "OFF":
                            logLevel = LogLevel.OFF;
                            break;
                        default:
                            logLevel = LogLevel.INFO;
                            break;
                    }
                    var currentAppdataPath = Path.Combine(AppdataPath, processName);
                    Logger.Instance.LoadConfig(currentAppdataPath + $"\\log\\Main", processName, logLevel);
                }
            }
            catch
            {
                return false;
            }
            finally
            {
                if (key != null)
                {
                    key.Dispose();
                    key.Close();
                }
            }
            return true;
        }

        /// <summary>
        /// Load config
        /// </summary>
        /// <param name="appName"></param>
        /// <param name="logFile"></param>
        /// <param name="logRepository"></param>
        [SecurityPermission(SecurityAction.Demand, Flags = SecurityPermissionFlag.ControlAppDomain)]
        public void LoadConfig(string logFullpath, string logRepository, LogLevel logLevel = LogLevel.OFF)
        {
            _isLoaded = false;

            string logFullname = logFullpath + logRepository + DateTime.Now.ToString("yyyyMMdd") + ".DotNet.log";

            try
            {
                //Configuration is not loaded not 
                if (logRepository == null)
                {
                    /* This method doesn't support the .NET Standard API. Not really used anyway so I'm not sure if this works.
                    BasicConfigurator.Configure();
                    _log = LogManager.GetLogger("Log4net.Default");
                    */
                    _log = LogManager.GetLogger(Assembly.GetExecutingAssembly(), "Log4net.Default");
                    _isLoaded = true;
                    return;
                }

                //configurate the logger;
                //create a rollingFileAppender;
                var rollingFileAppender = new RollingFileAppender();
                rollingFileAppender.AppendToFile = false;

                //max file size is 5MB.
                rollingFileAppender.MaxFileSize = 20 * 1024 * 1024;

                //set file default encoding, log4net use Encoding.Default as default encoding inappropriately 
                rollingFileAppender.Encoding = Encoding.UTF8;

                //max log count is 3.
                rollingFileAppender.MaxSizeRollBackups = 3;

                //rolling setting.
                rollingFileAppender.RollingStyle = RollingFileAppender.RollingMode.Size;

                //set log path.
                rollingFileAppender.File = logFullname;

                //log layout.
                rollingFileAppender.Layout = new PatternLayout("%date [%thread] %-5level - %message%newline");

                //immediate flush to hard disk.
                rollingFileAppender.ImmediateFlush = true;
                rollingFileAppender.ActivateOptions();

                //create a repository.
                _repository = LogManager.CreateRepository(logRepository);

                switch (logLevel)
                {
                    case LogLevel.DEBUG:
                        _repository.Threshold = Level.Debug;
                        break;
                    case LogLevel.ERROR:
                        _repository.Threshold = Level.Error;
                        break;
                    case LogLevel.FATAL:
                        _repository.Threshold = Level.Fatal;
                        break;
                    case LogLevel.WARN:
                        _repository.Threshold = Level.Warn;
                        break;
                    case LogLevel.INFO:
                        _repository.Threshold = Level.Info;
                        break;
                    case LogLevel.OFF:
                        _repository.Threshold = Level.Off;
                        break;
                    default:
                        _repository.Threshold = Level.Error;
                        break;
                }

                //configurate 
                BasicConfigurator.Configure(_repository, rollingFileAppender);
                _log = LogManager.GetLogger(logRepository, "Logger");

                AppDomain.CurrentDomain.UnhandledException += UnhandledExceptionHandler;

                _isLoaded = true;
            }
            catch
            {
                _isLoaded = false;
            }
        }


        public bool RefreshLogLevel(LogLevel logLevel)
        {
            try
            {
                if (_repository == null)
                {
                    return false;
                }

                switch (logLevel)
                {
                    case LogLevel.DEBUG:
                        _repository.Threshold = Level.Debug;
                        break;
                    case LogLevel.ERROR:
                        _repository.Threshold = Level.Error;
                        break;
                    case LogLevel.FATAL:
                        _repository.Threshold = Level.Fatal;
                        break;
                    case LogLevel.WARN:
                        _repository.Threshold = Level.Warn;
                        break;
                    case LogLevel.INFO:
                        _repository.Threshold = Level.Info;
                        break;
                    case LogLevel.OFF:
                        _repository.Threshold = Level.Off;
                        break;
                    default:
                        _repository.Threshold = Level.Error;
                        break;
                }

                //configurate 
                BasicConfigurator.Configure(_repository);
            }
            catch
            {
                return false;
            }
            return true;
        }


        /// <summary>
        /// 
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="args"></param>
        private void UnhandledExceptionHandler(object sender, UnhandledExceptionEventArgs args)
        {
            Exception ex = (Exception)args.ExceptionObject;

            _sInstance.Fatal("App", "UnhandledExceptionHandler", ex, "Applicaton crash!!!");
        }

        #region private members
        private static Logger _sInstance;
        private static object _slock = new object();
        private static string messageFormat = "{0}::{1}:\r\n{2}";

        private static string messageFormatWithExceptinCallStack =
            "{0}::{1}:\r\n{2}\r\n\tException:\t{3}\r\n\tMessage:\t{4}\r\n\tCallStack:\r\n\t{5}";

        private static string messageFormatWithInnerExceptinCallStack =
            "{0}::{1}:\r\n{2}\r\n\tException:\t{3}\r\n\tMessage:\t{4}\r\n\tCallStack:\r\n\t{5}\r\n\tInner Ex:\t{6}\r\n\tMessage:\t{7}\r\n\tCallStack:\r\n\t{8}";

        private ILog _log = null;
        private bool _isLoaded = false;

        #endregion

    }
}
